{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "34c2df92",
   "metadata": {},
   "source": [
    "# 시간 가중 벡터저장소 리트리버(TimeWeightedVectorStoreRetriever)\n",
    "\n",
    "`TimeWeightedVectorStoreRetriever` 는 의미론적 유사성과 시간에 따른 감쇠를 결합해 사용하는 검색 도구입니다. 이를 통해 문서 또는 데이터의 **\"신선함\"** 과 **\"관련성\"** 을 모두 고려하여 결과를 제공합니다.\n",
    "\n",
    "스코어링 알고리즘은 다음과 같이 구성됩니다\n",
    "\n",
    "$\\text{semantic\\_similarity} + (1.0 - \\text{decay\\_rate})^{hours\\_passed}$\n",
    "\n",
    "여기서 `semantic_similarity` 는 문서 또는 데이터 간의 의미적 유사도를 나타내고, `decay_rate` 는 시간이 지남에 따라 점수가 얼마나 감소하는지를 나타내는 비율입니다. `hours_passed` 는 객체가 마지막으로 접근된 후부터 현재까지 경과한 시간(시간 단위)을 의미합니다.\n",
    "\n",
    "이 방식의 주요 특징은, 객체가 마지막으로 접근된 시간을 기준으로 하여 **\"정보의 신선함\"** 을 평가한다는 점입니다. 즉, **자주 접근되는 객체는 시간이 지나도 높은 점수**를 유지하며, 이를 통해 **자주 사용되거나 중요하게 여겨지는 정보가 검색 결과 상위에 위치할 가능성이 높아집니다.** 이런 방식은 최신성과 관련성을 모두 고려하는 동적인 검색 결과를 제공합니다.\n",
    "\n",
    "특히, `decay_rate` 는 리트리버의 객체가 생성된 이후가 아니라 **마지막으로 액세스된 이후 경과된 시간** 을 의미합니다. 즉, 자주 액세스하는 객체는 '최신'으로 유지됩니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4885fe96",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4ff4366d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH10-Retriever\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c902d1d",
   "metadata": {},
   "source": [
    "## 낮은 감쇠율(low decay_rate)\n",
    "\n",
    "- `decay rate` 가 낮다는 것은 (여기서는 극단적으로 0에 가깝게 설정할 것입니다) **기억이 더 오래 \"기억될\"** 것임을 의미합니다.\n",
    "\n",
    "- `decay rate` 가 **0 이라는 것은 기억이 절대 잊혀지지 않는다**는 것을 의미하며, 이는 이 retriever를 vector lookup과 동등하게 만듭니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebfa7615",
   "metadata": {},
   "source": [
    "`TimeWeightedVectorStoreRetriever`를 초기화하며, 벡터 저장소, 감쇠율(`decay_rate`)을 매우 작은 값으로 설정하고, 검색할 벡터의 개수(k)를 1로 지정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7c335260",
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import datetime, timedelta\n",
    "\n",
    "import faiss\n",
    "from langchain.docstore import InMemoryDocstore\n",
    "from langchain.retrievers import TimeWeightedVectorStoreRetriever\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.documents import Document\n",
    "from langchain_openai import OpenAIEmbeddings\n",
    "\n",
    "# 임베딩 모델을 정의합니다.\n",
    "embeddings_model = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
    "\n",
    "# 벡터 저장소를 빈 상태로 초기화합니다.\n",
    "embedding_size = 1536\n",
    "index = faiss.IndexFlatL2(embedding_size)\n",
    "vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {})\n",
    "\n",
    "# 시간 가중치가 적용된 벡터 저장소 검색기를 초기화합니다. (여기서는, 낮은 감쇠율을 적용합니다)\n",
    "retriever = TimeWeightedVectorStoreRetriever(\n",
    "    vectorstore=vectorstore, decay_rate=0.0000000000000000000000001, k=1\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71553731",
   "metadata": {},
   "source": [
    "간단한 예제 데이터를 추가합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51d7ad5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 어제 날짜를 계산합니다.\n",
    "yesterday = datetime.now() - timedelta(days=1)\n",
    "\n",
    "retriever.add_documents(\n",
    "    # 문서를 추가하고, metadata에 어제 날짜를 설정합니다.\n",
    "    [\n",
    "        Document(\n",
    "            page_content=\"테디노트 구독해 주세요.\",\n",
    "            metadata={\"last_accessed_at\": yesterday},\n",
    "        )\n",
    "    ]\n",
    ")\n",
    "\n",
    "# 다른 문서를 추가합니다. metadata는 별도로 설정하지 않았습니다.\n",
    "retriever.add_documents([Document(page_content=\"테디노트 구독 해주실꺼죠? Please!\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8e1fe4f",
   "metadata": {},
   "source": [
    "`retriever.invoke()` 를 호출하여 검색을 수행합니다.\n",
    "\n",
    "- 이는 가장 두드러진(salient) 문서이기 때문입니다.\n",
    "- `decay_rate` 가 **0에 가깝기 때문** 에 문서는 여전히 최신(recent)으로 간주됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "63912716",
   "metadata": {},
   "outputs": [],
   "source": [
    "# \"테디노트 구독해 주세요.\" 가 가장 먼저 반환되는 이유는 가장 두드러지기 때문이며\n",
    "# 감쇠율이 0에 가깝기 때문에 여전히 최신 상태를 유지하고 있음을 의미합니다.\n",
    "retriever.invoke(\"테디노트\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00c9b59e",
   "metadata": {},
   "source": [
    "## 높음 감쇠율(high decay_rate)\n",
    "\n",
    "높은 `decay_rate`(예: 0.9999...)를 사용하면 `recency score`가 빠르게 0으로 수렴합니다.\n",
    "\n",
    "(만약 이 값을 1로 설정하면 모든 객체의 `recency` 값이 0이 되어, Vector Lookup 과 동일한 결과를 얻게 됩니다.)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b64543a",
   "metadata": {},
   "source": [
    "`TimeWeightedVectorStoreRetriever`를 사용하여 검색기를 초기화합니다. `decay_rate`를 0.999로 설정하여 시간에 따른 가중치 감소율을 조정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2891e7d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 임베딩 모델을 정의합니다.\n",
    "embeddings_model = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
    "\n",
    "# 벡터 저장소를 빈 상태로 초기화합니다.\n",
    "embedding_size = 1536\n",
    "index = faiss.IndexFlatL2(embedding_size)\n",
    "vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {})\n",
    "\n",
    "# 시간 가중치가 적용된 벡터 저장소 검색기를 초기화합니다.\n",
    "retriever = TimeWeightedVectorStoreRetriever(\n",
    "    vectorstore=vectorstore, decay_rate=0.999, k=1\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "70520527",
   "metadata": {},
   "source": [
    "다시 문서를 새롭게 추가합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "227a4317",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 어제 날짜를 계산합니다.\n",
    "yesterday = datetime.now() - timedelta(days=1)\n",
    "\n",
    "retriever.add_documents(\n",
    "    # 문서를 추가하고, metadata에 어제 날짜를 설정합니다.\n",
    "    [\n",
    "        Document(\n",
    "            page_content=\"테디노트 구독해 주세요.\",\n",
    "            metadata={\"last_accessed_at\": yesterday},\n",
    "        )\n",
    "    ]\n",
    ")\n",
    "\n",
    "# 다른 문서를 추가합니다. metadata는 별도로 설정하지 않았습니다.\n",
    "retriever.add_documents([Document(page_content=\"테디노트 구독 해주실꺼죠? Please!\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6293611d",
   "metadata": {},
   "source": [
    "`retriever.invoke(\"테디노트\")` 를 호출하면 `\"\"테디노트 구독 해주실꺼죠? Please!\"\"` 가 먼저 반환됩니다.\n",
    "- 이는 retriever가 \"테디노트 구독해 주세요.\" 와 관련된 문서를 대부분 잊어버렸기 때문입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c85ae8b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 검색 후 결과확인\n",
    "retriever.invoke(\"테디노트\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e625206",
   "metadata": {},
   "source": [
    "## 감쇠율(decay_rate) 정리\n",
    "\n",
    "- `decay_rate` 를 0.000001 로 매우 작게 설정한 경우\n",
    "  - 감쇠율(즉, 정보를 망각하는 비율)이 매우 낮기 때문에 정보를 거의 잊지 않습니다. \n",
    "  - 따라서, **최신 정보이든 오래된 정보든 시간 가중치 차이가 거의 없습니다.** 이럴때는 유사도에 더 높은 점수를 주게 됩니다.\n",
    "\n",
    "- `decay_rate` 를 0.999 로 1에 가깝게 설정한 경우 \n",
    "  - 감쇠율(즉, 정보를 망각하는 비율)이 매우 높습니다. 따라서, 과거의 정보는 거의다 잊어버립니다. \n",
    "  - 따라서, 이러한 경우는 최신 정보에 더 높은 점수를 주게 됩니다."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3ad69d5",
   "metadata": {},
   "source": [
    "## 가상의 시간으로 `decay_rate` 조정\n",
    "\n",
    "LangChain의 일부 유틸리티를 사용하면 시간 구성 요소를 모의(mock) 테스트 할 수 있습니다.\n",
    "\n",
    "- `mock_now` 함수는 LangChain에서 제공하는 유틸리티 함수로, 현재 시간을 모의(mock)하는 데 사용됩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "081e96d0",
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime\n",
    "\n",
    "from langchain.utils import mock_now\n",
    "\n",
    "# 현재 시간을 특정 시점으로 설정\n",
    "mock_now(datetime.datetime(2024, 8, 30, 00, 00))\n",
    "\n",
    "# 현재 시간 출력\n",
    "print(datetime.datetime.now())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c13f983",
   "metadata": {},
   "source": [
    "`mock_now` 함수를 사용하여 현재 시간을 변경하면서 검색 결과를 테스트할 수 있습니다. \n",
    "\n",
    "- 해당 기능을 활용하여 적절한 `decay_rate` 를 찾는데 도움을 받을 수 있습니다.\n",
    "\n",
    "[주의] 만약 너무 오래전의 시간으로 설정하면, decay_rate 계산시 오류가 발생할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "96bebb2e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 현재 시간을 임의의 시간으로 변경합니다.\n",
    "with mock_now(datetime.datetime(2024, 8, 29, 00, 00)):\n",
    "    # 변경된 시점에서 문서를 검색합니다.\n",
    "    print(retriever.invoke(\"테디노트\"))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
